/**
 * @file lv_mem.c
 * General and portable implementation of malloc and free.
 * The dynamic memory monitoring is also supported.
 */

/*********************
 *      INCLUDES
 *********************/
#include "lv_mem.h"
#include "lv_math.h"
#include <Library/minstring.h>

#if LV_MEM_CUSTOM != 0
#include LV_MEM_CUSTOM_INCLUDE
#endif

/*********************
 *      DEFINES
 *********************/
#define LV_MEM_ADD_JUNK                                                        \
  0 /*Add memory junk on alloc (0xaa) and free(0xbb) (just for testing         \
       purposes)*/

#ifdef LV_MEM_ENV64
#define MEM_UNIT uint64_t
#else
#define MEM_UNIT uint32_t
#endif

/**********************
 *      TYPEDEFS
 **********************/

/*The size of this union must be 4 bytes (uint32_t)*/
typedef union {
  struct {
    MEM_UNIT used : 1;    // 1: if the entry is used
    MEM_UNIT d_size : 31; // Size off the data (1 means 4 bytes)
  };
  MEM_UNIT header; // The header (used + d_size)
} lv_mem_header_t;

typedef struct {
  lv_mem_header_t header;
  uint8_t first_data; /*First data byte in the allocated data (Just for easily
                         create a pointer)*/
} lv_mem_ent_t;

/**********************
 *  STATIC PROTOTYPES
 **********************/
#if LV_MEM_CUSTOM == 0
static lv_mem_ent_t *ent_get_next(lv_mem_ent_t *act_e);
static void *        ent_alloc(lv_mem_ent_t *e, uint32_t size);
static void          ent_trunc(lv_mem_ent_t *e, uint32_t size);
#endif

/**********************
 *  STATIC VARIABLES
 **********************/
#if LV_MEM_CUSTOM == 0
static LV_MEM_ATTR MEM_UNIT
                   work_mem_int[LV_MEM_SIZE / sizeof(MEM_UNIT)]; /*Work memory for
                                                                    allocations*/
static uint8_t *work_mem;
#endif

static uint32_t zero_mem; /*Give the address of this variable if 0 byte should
                             be allocated*/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

/**
 * Initiaiize the dyn_mem module (work memory and other variables)
 */
void lv_mem_init(void)
{
#if LV_MEM_CUSTOM == 0
  work_mem           = (uint8_t *)work_mem_int;
  lv_mem_ent_t *full = (lv_mem_ent_t *)work_mem;
  full->header.used  = 0;
  /*The total mem size id reduced by the first header and the close patterns */
  full->header.d_size = LV_MEM_SIZE - sizeof(lv_mem_header_t);
#endif
}

/**
 * Allocate a memory dynamically
 * @param size size of the memory to allocate in bytes
 * @return pointer to the allocated memory
 */
void *lv_mem_alloc(uint32_t size)
{
  if (size == 0) {
    return &zero_mem;
  }

#ifdef LV_MEM_ENV64
  /*Round the size up to 8*/
  if (size & 0x7) {
    size = size & (~0x7);
    size += 8;
  }
#else
  /*Round the size up to 4*/
  if (size & 0x3) {
    size = size & (~0x3);
    size += 4;
  }
#endif
  void *alloc = NULL;

#if LV_MEM_CUSTOM == 0 /*Use the allocation from dyn_mem*/
  lv_mem_ent_t *e = NULL;

  // Search for a appropriate entry
  do {
    // Get the next entry
    e = ent_get_next(e);

    /*If there is next entry then try to allocate there*/
    if (e != NULL) {
      alloc = ent_alloc(e, size);
    }
    // End if there is not next entry OR the alloc. is successful
  } while (e != NULL && alloc == NULL);

#if LV_MEM_ADD_JUNK
  if (alloc != NULL)
    memset(alloc, 0xaa, size);
#endif

#else /*Use custom, user defined malloc function*/
  /*Allocate a header too to store the size*/
  alloc = LV_MEM_CUSTOM_ALLOC(size + sizeof(lv_mem_header_t));
  if (alloc != NULL) {
    ((lv_mem_ent_t *)alloc)->header.d_size = size;
    ((lv_mem_ent_t *)alloc)->header.used   = 1;
    alloc = &((lv_mem_ent_t *)alloc)->first_data;
  }
#endif

  if (alloc == NULL)
    LV_LOG_WARN("Couldn't allocate memory");

  return alloc;
}

/**
 * Free an allocated data
 * @param data pointer to an allocated memory
 */
void lv_mem_free(const void *data)
{
  if (data == &zero_mem)
    return;
  if (data == NULL)
    return;

#if LV_MEM_ADD_JUNK
  memset((void *)data, 0xbb, lv_mem_get_size(data));
#endif

  /*e points to the header*/
  lv_mem_ent_t *e = (lv_mem_ent_t *)((uint8_t *)data - sizeof(lv_mem_header_t));
  e->header.used  = 0;

#if LV_MEM_CUSTOM == 0
#if LV_MEM_AUTO_DEFRAG
  /* Make a simple defrag.
   * Join the following free entries after this*/
  lv_mem_ent_t *e_next;
  e_next = ent_get_next(e);
  while (e_next != NULL) {
    if (e_next->header.used == 0) {
      e->header.d_size += e_next->header.d_size + sizeof(e->header);
    }
    else {
      break;
    }
    e_next = ent_get_next(e_next);
  }
#endif
#else /*Use custom, user defined free function*/
  LV_MEM_CUSTOM_FREE(e);
#endif
}

/**
 * Reallocate a memory with a new size. The old content will be kept.
 * @param data pointer to an allocated memory.
 * Its content will be copied to the new memory block and freed
 * @param new_size the desired new size in byte
 * @return pointer to the new memory
 */
void *lv_mem_realloc(void *data_p, uint32_t new_size)
{
  /*data_p could be previously freed pointer (in this case it is invalid)*/
  if (data_p != NULL) {
    lv_mem_ent_t *e =
        (lv_mem_ent_t *)((uint8_t *)data_p - sizeof(lv_mem_header_t));
    if (e->header.used == 0) {
      data_p = NULL;
    }
  }

  uint32_t old_size = lv_mem_get_size(data_p);
  if (old_size == new_size)
    return data_p; /*Also avoid reallocating the same memory*/

#if LV_MEM_CUSTOM == 0
  /* Only truncate the memory is possible
   * If the 'old_size' was extended by a header size in 'ent_trunc' it avoids
   * reallocating this same memory */
  if (new_size < old_size) {
    lv_mem_ent_t *e =
        (lv_mem_ent_t *)((uint8_t *)data_p - sizeof(lv_mem_header_t));
    ent_trunc(e, new_size);
    return &e->first_data;
  }
#endif

  void *new_p;
  new_p = lv_mem_alloc(new_size);

  if (new_p != NULL && data_p != NULL) {
    /*Copy the old data to the new. Use the smaller size*/
    if (old_size != 0) {
      memcpy(new_p, data_p, LV_MATH_MIN(new_size, old_size));
      lv_mem_free(data_p);
    }
  }

  if (new_p == NULL)
    LV_LOG_WARN("Couldn't allocate memory");

  return new_p;
}

/**
 * Join the adjacent free memory blocks
 */
void lv_mem_defrag(void)
{
#if LV_MEM_CUSTOM == 0
  lv_mem_ent_t *e_free;
  lv_mem_ent_t *e_next;
  e_free = ent_get_next(NULL);

  while (1) {
    /*Search the next free entry*/
    while (e_free != NULL) {
      if (e_free->header.used != 0) {
        e_free = ent_get_next(e_free);
      }
      else {
        break;
      }
    }

    if (e_free == NULL)
      return;

    /*Joint the following free entries to the free*/
    e_next = ent_get_next(e_free);
    while (e_next != NULL) {
      if (e_next->header.used == 0) {
        e_free->header.d_size += e_next->header.d_size + sizeof(e_next->header);
      }
      else {
        break;
      }

      e_next = ent_get_next(e_next);
    }

    if (e_next == NULL)
      return;

    /*Continue from the lastly checked entry*/
    e_free = e_next;
  }
#endif
}

/**
 * Give information about the work memory of dynamic allocation
 * @param mon_p pointer to a dm_mon_p variable,
 *              the result of the analysis will be stored here
 */
void lv_mem_monitor(lv_mem_monitor_t *mon_p)
{
  /*Init the data*/
  memset(mon_p, 0, sizeof(lv_mem_monitor_t));
#if LV_MEM_CUSTOM == 0
  lv_mem_ent_t *e;
  e = NULL;

  e = ent_get_next(e);

  while (e != NULL) {
    if (e->header.used == 0) {
      mon_p->free_cnt++;
      mon_p->free_size += e->header.d_size;
      if (e->header.d_size > mon_p->free_biggest_size) {
        mon_p->free_biggest_size = e->header.d_size;
      }
    }
    else {
      mon_p->used_cnt++;
    }

    e = ent_get_next(e);
  }
  mon_p->total_size = LV_MEM_SIZE;
  mon_p->used_pct   = 100 - (100U * mon_p->free_size) / mon_p->total_size;
  mon_p->frag_pct =
      (uint32_t)mon_p->free_biggest_size * 100U / mon_p->free_size;
  mon_p->frag_pct = 100 - mon_p->frag_pct;
#endif
}

/**
 * Give the size of an allocated memory
 * @param data pointer to an allocated memory
 * @return the size of data memory in bytes
 */
uint32_t lv_mem_get_size(const void *data)
{
  if (data == NULL)
    return 0;
  if (data == &zero_mem)
    return 0;

  lv_mem_ent_t *e = (lv_mem_ent_t *)((uint8_t *)data - sizeof(lv_mem_header_t));

  return e->header.d_size;
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

#if LV_MEM_CUSTOM == 0
/**
 * Give the next entry after 'act_e'
 * @param act_e pointer to an entry
 * @return pointer to an entry after 'act_e'
 */
static lv_mem_ent_t *ent_get_next(lv_mem_ent_t *act_e)
{
  lv_mem_ent_t *next_e = NULL;

  if (act_e == NULL) { /*NULL means: get the first entry*/
    next_e = (lv_mem_ent_t *)work_mem;
  }
  else { /*Get the next entry */
    uint8_t *data = &act_e->first_data;
    next_e        = (lv_mem_ent_t *)&data[act_e->header.d_size];

    if (&next_e->first_data >= &work_mem[LV_MEM_SIZE])
      next_e = NULL;
  }

  return next_e;
}

/**
 * Try to do the real allocation with a given size
 * @param e try to allocate to this entry
 * @param size size of the new memory in bytes
 * @return pointer to the allocated memory or NULL if not enough memory in the
 * entry
 */
static void *ent_alloc(lv_mem_ent_t *e, uint32_t size)
{
  void *alloc = NULL;

  /*If the memory is free and big enough then use it */
  if (e->header.used == 0 && e->header.d_size >= size) {
    /*Truncate the entry to the desired size */
    ent_trunc(e, size),

        e->header.used = 1;

    /*Save the allocated data*/
    alloc = &e->first_data;
  }

  return alloc;
}

/**
 * Truncate the data of entry to the given size
 * @param e Pointer to an entry
 * @param size new size in bytes
 */
static void ent_trunc(lv_mem_ent_t *e, uint32_t size)
{
#ifdef LV_MEM_ENV64
  /*Round the size up to 8*/
  if (size & 0x7) {
    size = size & (~0x7);
    size += 8;
  }
#else
  /*Round the size up to 4*/
  if (size & 0x3) {
    size = size & (~0x3);
    size += 4;
  }
#endif

  /*Don't let empty space only for a header without data*/
  if (e->header.d_size == size + sizeof(lv_mem_header_t)) {
    size = e->header.d_size;
  }

  /* Create the new entry after the current if there is space for it */
  if (e->header.d_size != size) {
    uint8_t *     e_data      = &e->first_data;
    lv_mem_ent_t *after_new_e = (lv_mem_ent_t *)&e_data[size];
    after_new_e->header.used  = 0;
    after_new_e->header.d_size =
        e->header.d_size - size - sizeof(lv_mem_header_t);
  }

  /* Set the new size for the original entry */
  e->header.d_size = size;
}

#endif
